
Screenshot: Hanging in the Air the Way Bricks Don't
文件:
•. t12.rb
•. cannon.rb
•. lcdrange.rb
在这个示例里,我们扩展了LCDRange类,在其中加入了一个文字标签。我们还加入了一个射击目标。
def initialize(s, parent = nil)
super(parent)
init()
setText(s)
end
构造函数首先调用init(),然后设置标签上的文字。init()是一个用于做初始化的独立的函数,把它单独作为一个函数主要是因为这个教程的原始C++版本里面是使用函数重载来实现初始化的。
def init()
lcd = Qt::LCDNumber.new(2)
lcd.setSegmentStyle(Qt::LCDNumber::Filled)
@slider = Qt::Slider.new(Qt::Horizontal)
@slider.setRange(0, 99)
@slider.setValue(0)
@label = Qt::Label.new()
@label.setAlignment(Qt::AlignHCenter.to_i | Qt::AlignTop.to_i)
connect(@slider, SIGNAL('valueChanged(int)'),
lcd, SLOT('display(int)'))
connect(@slider, SIGNAL('valueChanged(int)'),
self, SIGNAL('valueChanged(int)'))
layout = Qt::VBoxLayout.new()
layout.addWidget(lcd)
layout.addWidget(@slider)
layout.addWidget(@label)
setLayout(layout)
setFocusProxy(@slider)
end
对lcd和slider的设置代码是与前一章相同的。接下来我们创建一个Qt::Label,并且让它的内容在水平方向居中显示,在竖直方向置顶显示。那些Qt::Object::connect()代码也是从前一章中直接山寨过来的。
def setText(s)
@label.setText(s)
end
这个函数设置文本标签的文字内容。
CannonField现在有了两个新的信号:hit()和missed()。另外,它还会记录一个射击目标。
signals 'hit()', 'missed()' #...
当子弹命中目标时,就会发射hit()信号。当子弹飞出了本部件的右边界或底部边界时(也就是说,已经确定它没有命中也不会再命中目标了),会发射missed()信号。
newTarget()
这行代码加入到了构造函数中。它为目标物体计算出一个“随机”位置。事实上,newTarget()函数会尝试着绘制出目标物体。因为我们此时还是在构造函数的代码中,所以CannonField部件此时还是不可见的。Qt能够确保当妳在一个隐藏部件上调用Qt::Widget::update()时不会产生破坏效果。
@@first_time = true
def newTarget()
if @@first_time
@@first_time = false
midnight = Qt::Time.new(0, 0, 0)
srand(midnight.secsTo(Qt::Time.currentTime()))
end
@target = Qt::Point.new(200 + rand(190), 10 + rand(255))
update()
end
这个函数创建一个其中心点位于随机位置的目标物体。
我们创建一个Qt::Time对象midnight,它表示的时间是00:00:00。然后,我们计算出自午夜到现在的秒数,将这个秒数作为随机数种子。参考Qt::Date、Qt::Time和Qt::DateTime的文档,以了解更多信息。
最后,我们计算出目标的中心点。我们保持它位于(x = 200, y = 35, width = 190, height = 255)(也就是说,x和y的取值范围分别是200到389和35到289)这个矩形中,所使用的是这样的坐标系统:y坐标轴的位置0位于本部件的底部边缘,并且y的值向上增加,x是正常的,0位于左边缘,x的值向右增加。
我们已经做过试验,子弹能够达到这整个范围。
def moveShot()
region = Qt::Region.new(shotRect())
@timerCount += 1
shotR = shotRect()
这部分的定时器事件代码跟前一章中相同。
if shotR.intersects(targetRect())
@autoShootTimer.stop()
emit hit()
这段if语句会检查子弹的矩形区域是否与目标的矩形区域相交。如果是相交的,那么子弹就已经命中目标(精彩!)。我们停掉子弹定时器,并且发射hit()信号,向外界告知某个目标已经被摧毁,然后返回。注意,我们完全可以做到当场再创建一个新的目标,但是,由于CannonField是一个组件,所以,我们将决定权留给此组件的用户。
elsif shotR.x() > width() || shotR.y() > height()
@autoShootTimer.stop()
emit missed()
这段代码与前一章类似,不同点就在于它会发射missed()信号,以向外界告知打炮失败的消息。
else
region = region.unite(Qt::Region.new(shotR))
end
update(region)
end
然后,这个函数剩下的部分就跟之前章节一样。
CannonField::paintEvent()与之前章节一样,只是加入了这一行:
paintTarget(painter)
这行代码会确保,在必要的时候也会绘制射击目标。
def paintTarget(painter)
painter.setBrush(Qt::Brush.new(Qt::red))
painter.setPen(Qt::Pen.new(Qt::Color.new(Qt::black)))
painter.drawRect(targetRect())
end
这个函数会绘制射击目标;即为一个边缘为黑色、由红色填充的矩形。
def targetRect()
result = Qt::Rect.new(0, 0, 20, 10)
result.moveCenter(Qt::Point.new(@target.x(), height() - 1 - @target.y()))
return result
end
这个私有函数会返回射击目标的矩形区域。还记得吗,在newTarget()中,target这个坐标点的计算过程中,会将y轴的0坐标放置在本部件的底部。在我们调用Qt::Rect::moveCenter()之前,先按照部件本身的坐标系来计算出这个点的坐标。
我们选择采用这种坐标系映射手段的原因就是,为了让射击目标与部件底部之间的距离保持固定。别忘了,本部件可能在任何时候被用户或程序本身改变大小。
在MyWidget 类中没有新的成员,但是我们稍微改变了一下构造函数,以设置新的LCDRange 对象的文本标签文字。
angle = LCDRange.new(tr('ANGLE'))
我们将角度文字标签的内容设置成"ANGLE"。
force = LCDRange.new(tr('FORCE'))
我们将力道文字标签的内容设置成"FORCE"。
LCDRange部件看起来会有点奇怪:当我们改变MyWidget的大小时,Qt::VBoxLayout中的内置布局管理器会给文本标签分配太多的空间,而其它的部件却得不到足够的空间;导致两个LCDRange部件之间的空间会改变大小。我们将在下一章中解决这个问题
创建一个作弊按钮,当这个按钮被按时,让CannonField显示出射击策略,并且显示5秒。
如果妳做了前一章中的"圆形子弹"练习的话,那么,这次试着将shotRect()改变成shotRegion(),返回一个Qt::Region,以实现更精确的碰撞检测。
创建一个移动的目标。
确保射击目标每次被创建时都整个处于屏幕上的可见区域。
确保这个部件不可被改变大小,这样射击目标就不会突然变得不可见了。[提示:可试试使用Qt::Widget::setMinimumSize()来做到这一点。]
这个有点难;让这个程序支持多个子弹同时处于空中飞翔。[提示:创建一个专门的Shot类。]
自适应阈值化
HxLauncher: Launch Android applications by voice commands